package uk.ljmu.qudexapps.swfspreadsheet.services;

import java.io.InputStream;
//import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;

import org.apache.log4j.Logger;
import org.springframework.binding.message.MessageBuilder;
import org.springframework.binding.message.MessageContext;
import org.springframework.stereotype.Service;

import uk.ljmu.metadata.schemas.SchemaInstanceContainer;
//import uk.ljmu.metadata.staticschemas.DoapSchema;
import uk.ljmu.metadata.staticschemas.FedoraRelsExtOntology;
import uk.ljmu.metadata.staticschemas.GeoSchema;
import uk.ljmu.metadata.staticschemas.StaticDDI;
import uk.ljmu.metadata.staticschemas.StaticJenaDublinCore;
import uk.ljmu.metadata.staticschemas.StaticJenaDublinCoreTerms;
//import uk.ljmu.metadata.staticschemas.StaticJenaFoaf;
import uk.ljmu.metadata.staticschemas.StaticJenaRDFS;
import uk.ljmu.metadata.staticschemas.StaticQudex;
import uk.ljmu.metadata.staticschemas.StaticSkos;
import uk.ljmu.qudexapps.swfspreadsheet.domain.Element;
import uk.ljmu.qudexapps.swfspreadsheet.domain.Elements;
import uk.ljmu.qudexapps.swfspreadsheet.domain.MdSchema;
import uk.ljmu.qudexapps.swfspreadsheet.domain.MdSchemaInstance;
import uk.ljmu.qudexapps.swfspreadsheet.domain.VocabularyElement;
import uk.ljmu.qudexapps.swfspreadsheet.utilities.Constants;
import uk.ljmu.qudexapps.swfspreadsheet.utilities.RdfFunctions;

import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.rdf.model.ModelFactory;
//import com.hp.hpl.jena.rdf.model.Property;

@Service("mdService")
public class MetadataServiceImpl implements MetadataService {

    /**
     * 
     */
    private static final long serialVersionUID = -6778253694655906722L;
    private boolean schemasPresent= false;
    private static Logger log = Logger.getLogger(MetadataServiceImpl.class);

    /**
     * 0 - Collections Spreadsheet 1 - Documents Spreadsheet
     */
    private int typeSS;

    /**
     * Holds the elements needed to generate either qudex collections or files
     * spreadsheets
     */
    private Elements qudexElements;

    /**
     * @param typeSS
     *            the typeSS to set
     */
    public void setTypeSS(int typeSS) {
	this.typeSS = typeSS;
    }

    /**
     * @return the typeSS
     */
    public int getTypeSS() {
	return typeSS;
    }

    /**
     * @param qudexElements
     *            the qudexElements to set
     */
    public void setQudexElements(Elements qudexElements) {
	this.qudexElements = qudexElements;
    }

    /**
     * @return the qudexElements
     */
    public Elements getQudexElements() {
	return qudexElements;
    }

    /**
     * List with name-sensible name pairs for each schema available - Object
     * MdSchema
     */
    private List<MdSchema> availableSchemas;

    /**
     * List holding the schemas selected by the user to include in the
     * spreadsheet List of MdSchemaInstance objects: name-sensible name + List
     * with selected elements from the particular schema
     */
    private List<MdSchemaInstance> mdSchemaInstanceContainer;

    /**
     * @return the mdSchemaInstanceContainer
     */
    public List<MdSchemaInstance> getMdSchemaInstanceContainer() {
	return mdSchemaInstanceContainer;
    }

    /**
     * @param mdSchemaInstanceContainer
     *            the mdSchemaInstanceContainer to set
     */
    public void setMdSchemaInstanceContainer(
	    List<MdSchemaInstance> mdSchemaInstanceContainer) {
	this.mdSchemaInstanceContainer = mdSchemaInstanceContainer;
    }

    public void setAvailableSchemas(List<MdSchema> availableSchemas) {
	this.availableSchemas = availableSchemas;
    }

    public List<MdSchema> getAvailableSchemas() {
	return availableSchemas;
    }

    public MetadataServiceImpl() {
	this.mdSchemaInstanceContainer = new ArrayList<MdSchemaInstance>();
	this.availableSchemas = new ArrayList<MdSchema>();
	// Assign Qudex Collections Spreadsheet by default
	this.typeSS = Constants.Q_COLLECTIONS_SS;
    }

    public List<MdSchemaInstance> getSelectedSchemas() {
	return this.mdSchemaInstanceContainer;
    }

    // METHODS FOR PARSING SCHEMAS
    /*
     * OLD METHODS*/   
    /**
     * Gets the elements of a given schema using the offline RDF version of the schema
     * @deprecated
     * @param schema schema URL
     * @return list of VocabularyElements representing the schema elements
     */
  /*  public List<VocabularyElement> getFieldsOfClass(String schema) {
	Class<?> aux_class = null;
	List<VocabularyElement> aux_list = new ArrayList<VocabularyElement>();

	if (schema.equalsIgnoreCase("DC")) {
	    StaticJenaDublinCore elem = new StaticJenaDublinCore();
	    aux_class = elem.getClass();
	} else if (schema.equalsIgnoreCase("FOAF")) {
	    StaticJenaFoaf elem = new StaticJenaFoaf();
	    aux_class = elem.getClass();
	} else if (schema.equalsIgnoreCase("RDFS")) {
	    StaticJenaRDFS elem = new StaticJenaRDFS();
	    aux_class = elem.getClass();
	} else if (schema.equalsIgnoreCase("REL")) {

	    FedoraRelsExtOntology elem = new FedoraRelsExtOntology();
	    aux_class = elem.getClass();

	} else if (schema.equalsIgnoreCase("GEO")) {

	    GeoSchema elem = new GeoSchema();
	    aux_class = elem.getClass();

	} else if (schema.equalsIgnoreCase("DOAP")) {

	    DoapSchema elem = new DoapSchema();
	    aux_class = elem.getClass();

	}
	// Get all object fields including public, protected, package and
	// private
	// access fields.
	if (aux_class != null) {
	    Field[] fields = aux_class.getDeclaredFields();

	    log.info("Number of fields = " + fields.length);
	    for (Field field : fields) {
		if (!field.getType().getName().equalsIgnoreCase(
			"com.hp.hpl.jena.rdf.model.Model")) {
		    VocabularyElement auxVoc = new VocabularyElement();
		    int index = field.getType().getName().lastIndexOf(".");
		    String type = field.getType().getName().substring(
			    index + 1, field.getType().getName().length());

		    if (type.equalsIgnoreCase("Property")
			    && !field.getName().equalsIgnoreCase("NAMESPACE")) {
			Property prop;
			try {
			    prop = (Property) field.get(null);
			    auxVoc.setName(prop.getLocalName());
			} catch (IllegalArgumentException e) {
			    log.fatal(e.getMessage());
			} catch (IllegalAccessException e) {
			    log.fatal(e.getMessage());
			}
			auxVoc.setType(type);
			auxVoc.setComment("");
			aux_list.add(auxVoc);
		    }
		}
	    }
	    return aux_list;
	}
	return null;
    }*/

    /**
     * Gets all the elements from a given metadata schema by using the online
     * RDF version of the schema
     * 
     * @deprecated
     * @param schema metadata schema URL
     * @return list of VocabularyElement instances representing the metadata elements
     */
    /*public List<VocabularyElement> getFieldsOfClassDynamic(String schema) {
	List<VocabularyElement> listSchemaElems = new ArrayList<VocabularyElement>();

	if (schema.equalsIgnoreCase("DC")) {
	    listSchemaElems = RdfFunctions
		    .readPropertiesFromSchema(RdfFunctions.DC_Schema);
	    if (listSchemaElems != null)
		normalizeNames(listSchemaElems);
	} else if (schema.equalsIgnoreCase("FOAF")) {
	    listSchemaElems = RdfFunctions
		    .readPropertiesFromSchema(RdfFunctions.FOAF_Schema);
	} else if (schema.equalsIgnoreCase("RDFS")) {
	    listSchemaElems = RdfFunctions
		    .readPropertiesFromSchema(RdfFunctions.RDFS_Schema);
	} else if (schema.equalsIgnoreCase("RELS")) {
	    listSchemaElems = RdfFunctions
		    .readPropertiesFromSchema(RdfFunctions.RELS_Schema);
	} else if (schema.equalsIgnoreCase("GEO")) {
	    listSchemaElems = RdfFunctions
		    .readPropertiesFromSchema(RdfFunctions.GEO_Schema);
	} else if (schema.equalsIgnoreCase("DOAP")) {
	    listSchemaElems = RdfFunctions
		    .readPropertiesFromSchema(RdfFunctions.DOAP_Schema);
	}

	if (listSchemaElems == null || listSchemaElems.size() == 0) {// Using
	    // Old method from constant class
	    log.info("Getting Schema elements offline, static method");
	    listSchemaElems = getFieldsOfClass(schema);
	}
	return listSchemaElems;
    }*/
    /*END OF OLD METHODS*/
    /**
     * Gets all the elements from a given metadata schema by using the online
     * RDF version of the schema
     * @param schema metadata schema URL
     * @return list of VocabularyElement instances representing the metadata elements
     */
    public List<VocabularyElement> getMetadataFieldsFromSchema(String prefix) {

	int index = this.findSchemaInstance(prefix);
	
	if (index == -1) {
	    List<VocabularyElement> listSchemaElems;
	    Model auxModel = this.getDynamicSchema(prefix);

	    listSchemaElems = RdfFunctions
		    .readPropertiesFromSchema(auxModel);		

	    if (listSchemaElems == null || listSchemaElems.size() == 0) {
		// AMG New method to obtain Model offline, from SchemaInstanceContainer class
		log.info("Getting Schema elements offline, static method");
		auxModel = SchemaInstanceContainer.findStaticSchemaModel(prefix);
		listSchemaElems = RdfFunctions
		    .readPropertiesFromSchema(auxModel);
	    } else {
		normalizeNames(listSchemaElems);
	    }
	    
	    return listSchemaElems;
	} else {
	    return this.mdSchemaInstanceContainer.get(index)
		    .getSelectedSchemaElements();
	}
    }
    
    /**
     * Adds a new MdSchemaInstance object to the list of metadata schemas
     * @param mdSchemaInstance the mdSchemaInstance to add
     * @return true is OK, false otherwise
     */
    public boolean addMetadataSchema(MdSchemaInstance mdSchemaInstance) {
	boolean bFound = false;
	try {
	    for (MdSchemaInstance instance : this.mdSchemaInstanceContainer) {
		if (instance.getPrefix().equalsIgnoreCase(mdSchemaInstance.getPrefix())) {
		    bFound = true;
		    if (!this.anyElementSelected(mdSchemaInstance.getSelectedSchemaElements()))
			instance.setRemoved(true);
		    else {
			instance.setRemoved(false);
			instance.setSelectedSchemaElements(mdSchemaInstance.getSelectedSchemaElements());
		    }
		    break;
		}
	    }
	    if (!bFound)
		this.mdSchemaInstanceContainer.add(mdSchemaInstance);
	} catch (Exception ex) {
	    return false;
	}
	return true;
    }
    private boolean anyElementSelected(
	    List<VocabularyElement> selectedSchemaElements) {

	for (VocabularyElement elem : selectedSchemaElements) {
	    //if (!elem.isOptional() && !elem.isSelected())
	//	elem.setSelected(true);
	    if (elem.isSelected())
		return true;
	}

	return false;
    }

    /**
     * Deletes a particular MdSchemaInstance Object identified by index
     * @param index int index of the deleted object
     * @return true is deleted successfully, false otherwise
     */
    public boolean deleteMetadataSchema(int index) {
	try {
	    this.mdSchemaInstanceContainer.remove(index);
	} catch (Exception ex) {
	    return false;
	}

	return true;
    }
    /**
     * Returns all the elements from a metadata schema identified by namespace prefix
     * @param prefix
     */
    //public List<VocabularyElement> getSchemaElements(String prefix) {
	//return this.getFieldsOfClassDynamic(prefix);
    //}
    /**
     * Returns a new MdSchemaInstance Object
     */
    public MdSchemaInstance createMdSchemaInstance() {
	return new MdSchemaInstance();
    }
    /**
     * Removes the unused resources
     */
    public void clean() {
	for (MdSchemaInstance instance : this.mdSchemaInstanceContainer) {
	    if (instance.getSchemaModel() != null
		    && !instance.getSchemaModel().isClosed())
		instance.getSchemaModel().close();
	}
	this.mdSchemaInstanceContainer.clear();
	this.setSchemasPresent(false);
    }
    /**
     * Deletes the selected Metadata Schema Instances
     * @param list of metadata prefixes to delete from the selected schemas
     */
    public void elimGroup(String list, MessageContext context) {
	if (list != null ) {
	    boolean valid = true;
	    String[] group = list.split(";");
	    // List<MdSchemaInstance> auxMdList = new ArrayList<MdSchemaInstance>();
	    if (group.length > 0 && group[0] != "")
        	    for (String deletedElement : group) {
        		int index = Integer.parseInt(deletedElement);
        		// auxMdList.add(this.mdSchemaInstanceContainer.get(index));
        		MdSchemaInstance instance = this.mdSchemaInstanceContainer.get(index);
        		
        		for(VocabularyElement selectedElem : instance.getSelectedSchemaElements()) {
        		    if (!selectedElem.isOptional()) {
        			valid = false;
        		    } else selectedElem.setSelected(false);
        		}
        		if (valid)
        		    this.mdSchemaInstanceContainer.get(index).setRemoved(true);
        		else {
        		    context.addMessage(new MessageBuilder().error().code("compulsoryElements").build());
        		}
        	    }
	}

	//for (MdSchemaInstance auxDsElem : auxMdList) {
	//    this.mdSchemaInstanceContainer.remove(auxDsElem);
	//}
    }
    
    /**
     * 
     * @param listSchemaElems
     */
    @SuppressWarnings("unused")
    private void addQudexInfoToElements(List<VocabularyElement> listSchemaElems) {
	for (VocabularyElement elem : listSchemaElems) {
	    for (Element qudexElem : this.qudexElements.getElement()) {
		if (elem.getName().equalsIgnoreCase(qudexElem.getName())) {
		    elem.setOptional(qudexElem.isOptional());
		    elem.setSelected(true);
		    break;
		}
	    }
	}
    }
    /**
     * 
     * @param prefix
     * @return
     */
    private Model getDynamicSchema(String prefix) {
	for (MdSchema schema : this.availableSchemas) {
	    if (schema.getPrefix().equalsIgnoreCase(prefix))
		return schema.getSchemaModel();
	}

	return null;
    }
    
    /**
     * Transforms all the metadata element names to lowercase
     * 
     * @param listSchemaElems list of metadata element names
     */
    private void normalizeNames(List<VocabularyElement> listSchemaElems) {
	for (VocabularyElement elem : listSchemaElems) {
	    elem.setLabel(elem.getLabel().toLowerCase());
	}
    }
    
    /**
     * Metadata Schemas Initialization
     */
    private void initialiseSchemas() {
	// ADD Dublin Core Schema
	Model auxModel = ModelFactory.createDefaultModel();
	MdSchema auxSchema = new MdSchema();
	try {
	    auxModel.read(RdfFunctions.DC_Schema);
	} catch (Exception e) {
	    log.info(e.getMessage());
	    // Static load
	    auxModel = StaticJenaDublinCore.getModel();
	}
	auxSchema.setSchemaModel(auxModel);
	auxSchema.setPrefix("dc");
	auxSchema.setReadableName("Dublin Core Vocabulary");
	this.availableSchemas.add(auxSchema);
	
	// ADD Geo W3C lat/long
	auxModel = ModelFactory.createDefaultModel();
	auxSchema = new MdSchema();
	try {
	    auxModel.read(RdfFunctions.GEO_Schema);
	} catch (Exception e) {
	    log.info(e.getMessage());
	    // Static load
	    auxModel = GeoSchema.getModel();
	}
	auxSchema.setSchemaModel(auxModel);
	auxSchema.setPrefix("geo");
	auxSchema.setReadableName("Basic Geo (WGS84 lat/long) Vocabulary");
	this.availableSchemas.add(auxSchema);
	
	// ADD RDF VOCABULARY
	auxModel = ModelFactory.createDefaultModel();
	auxSchema = new MdSchema();
	try {
	    auxModel.read(RdfFunctions.RDF_Schema);
	} catch (Exception e) {
	    log.info(e.getMessage());
	    // Static load
	    auxModel = StaticJenaRDFS.getModel();
	}
	auxSchema.setSchemaModel(auxModel);
	auxSchema.setPrefix("rdf");
	auxSchema.setReadableName("RDF Vocabulary");
	this.availableSchemas.add(auxSchema);
	
	// ADD DCTERMS VOCABULARY
	auxModel = ModelFactory.createDefaultModel();
	auxSchema = new MdSchema();
	try {
	    auxModel.read(RdfFunctions.DCTERMS_Schema);
	} catch (Exception e) {
	    log.info(e.getMessage());
	    // Static load
	    auxModel = StaticJenaDublinCoreTerms.getModel();
	}
	auxSchema.setSchemaModel(auxModel);
	auxSchema.setPrefix("dcterms");
	auxSchema.setReadableName("Dublin Core Terms");
	this.availableSchemas.add(auxSchema);
	
	// ADD Qudex VOCABULARY
	auxModel = ModelFactory.createDefaultModel();
	auxSchema = new MdSchema();
	try {
	    auxModel = StaticQudex.getModel();
	    auxSchema.setSchemaModel(auxModel);
	    auxSchema.setPrefix("qudex");
	    auxSchema.setReadableName("QuDex Vocabulary");
	    this.availableSchemas.add(auxSchema);
	} catch (Exception e) {
	    log.fatal(e.getMessage());
	}
	
	// ADD DDI2 VOCABULARY
	auxModel = ModelFactory.createDefaultModel();
	auxSchema = new MdSchema();
	try {
	    auxModel = StaticDDI.getModel();
	    auxSchema.setSchemaModel(auxModel);
	    auxSchema.setPrefix("ddi2");
	    auxSchema.setReadableName("DDI2 Vocabulary");
	    this.availableSchemas.add(auxSchema);
	} catch (Exception e) {
	    log.fatal(e.getMessage());
	}
	
	// ADD SKOS VOCABULARY
	auxModel = ModelFactory.createDefaultModel();
	auxSchema = new MdSchema();
	try {
	    auxModel.read(RdfFunctions.Skos_Schema);
	} catch (Exception e) {
	    log.info(e.getMessage());
	    // Static load
	    auxModel = StaticSkos.getModel();
	}
	auxSchema.setSchemaModel(auxModel);
	auxSchema.setPrefix("skos");
	auxSchema.setReadableName("Skos Vocabulary");
	this.availableSchemas.add(auxSchema);
	
	// ADD Fedora Relationships Ontology
	auxModel = ModelFactory.createDefaultModel();
	auxSchema = new MdSchema();
	try {
	    auxModel.read(RdfFunctions.RELS_Schema);
	} catch (Exception e) {
	    log.info(e.getMessage());
	    // Static load
	    auxModel = FedoraRelsExtOntology.getModel();
	}
	auxSchema.setSchemaModel(auxModel);
	auxSchema.setPrefix("rel");
	auxSchema.setReadableName("Fedora relationships Ontology");
	this.availableSchemas.add(auxSchema);
    }

    public boolean initialiseQudexSpreadsheetElements() {
	ClassLoader classLoader = Thread.currentThread()
		.getContextClassLoader();
	InputStream is;

	if (this.typeSS == Constants.Q_COLLECTIONS_SS) {
	    is = classLoader.getResourceAsStream("qudex-collection.xml");
	} else {
	    is = classLoader.getResourceAsStream("qudex-document.xml");
	}
	JAXBContext context;
	try {
	    context = JAXBContext.newInstance(Elements.class);
	    Unmarshaller unmarshaller = context.createUnmarshaller();
	    // validate against schema, note: setting schema to null will turn
	    // validator off
	    unmarshaller.setSchema(null);
	    this.setQudexElements(Elements.class.cast(unmarshaller
		    .unmarshal(is)));

	    return true;
	} catch (JAXBException e) {
	    log.fatal(e.getMessage());
	    return false;
	}
    }

    public void initialiseQudexElements() {
	MdSchemaInstance auxSchemaInstance;
	if (!this.isSchemasPresent()) {
	    initialiseQudexSpreadsheetElements();
	    List<VocabularyElement> auxVocab = new ArrayList<VocabularyElement>();
	    for (Element elem : this.qudexElements.getElement()) {
		if (elem.getNs() == null || elem.getNs().equals(""))
		    continue;
		int index = findSchemaInstance(elem.getNs());
		if (index > -1)
		    auxSchemaInstance = this.mdSchemaInstanceContainer.get(index);
		else {
		    auxSchemaInstance = new MdSchemaInstance();
		    auxSchemaInstance.setPrefix(elem.getNs());
		    auxSchemaInstance.setReadableName(Constants.readableNames
			.get(elem.getNs()));
		    this.mdSchemaInstanceContainer.add(auxSchemaInstance);
		    auxVocab = RdfFunctions.readPropertiesFromSchema(this
			.getDynamicSchema(elem.getNs()));
		    auxSchemaInstance.setSelectedSchemaElements(auxVocab);
		}
		auxSchemaInstance.setRemoved(false);
		VocabularyElement vElem = auxSchemaInstance
		    .getElementFromSelectedSchemas(elem.getName());
		if (vElem != null) {
		    vElem.setOptional(elem.isOptional());
		    vElem.setSelected(true);
		    vElem.setHidden(false);
		}
	    }
	}
    }

    private int findSchemaInstance(String ns) {
	int index = 0;
	for (MdSchemaInstance instance : this.getMdSchemaInstanceContainer()) {
	    if (instance.getPrefix().equalsIgnoreCase(ns))
		return index;
	    index++;
	}
	return -1;
    }
    
    public boolean schemasSelected() {
	for (MdSchemaInstance instance : this.mdSchemaInstanceContainer) {
	    if (!instance.isRemoved())
		return true;
	}
	return false;
    }

    /**
     * @param schemasPresent the schemasPresent to set
     */
    public void setSchemasPresent(boolean schemasPresent) {
	this.schemasPresent = schemasPresent;
    }

    /**
     * @return the schemasPresent
     */
    public boolean isSchemasPresent() {
	return schemasPresent;
    }
    
    public void destroy() {
	this.clean();
	for (MdSchema instance : this.availableSchemas) {
	    if (instance.getSchemaModel() != null && !instance.getSchemaModel().isClosed())
		instance.getSchemaModel().close();
	}
	
	this.availableSchemas.clear();
    }
    public void init() {
	this.initialiseSchemas();
    }
}
